﻿// This is an open source non-commercial project. Dear PVS-Studio, please check it.
// PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com

// ReSharper disable CheckNamespace
// ReSharper disable ClassNeverInstantiated.Global
// ReSharper disable CommentTypo
// ReSharper disable IdentifierTypo
// ReSharper disable InconsistentNaming
// ReSharper disable StringLiteralTypo
// ReSharper disable UnusedParameter.Local

/* IRun.cs --
 * Ars Magna project, http://arsmagna.ru
 */

#region Using directives

using System;
using System.Collections.Generic;
using System.Diagnostics;

#endregion

#nullable enable

namespace AM.Skia.RichTextKit.Utils;

/// <summary>
/// Interface to a run object with a start offset and a length
/// </summary>
public interface IRun
{
    /// <summary>
    /// Offset of the start of this run
    /// </summary>
    int Offset { get; }

    /// <summary>
    /// Length of this run
    /// </summary>
    int Length { get; }
}

/// <summary>
/// Represents a sub-run in a list of runs
/// </summary>
[DebuggerDisplay ("[{Index}] [{Offset} + {Length} = {Offset + Length}] {Partial}")]
public struct SubRun
{
    /// <summary>
    /// Constructs a new sub-run
    /// </summary>
    /// <param name="index">The run index</param>
    /// <param name="offset">The sub-run offset</param>
    /// <param name="length">The sub-run length</param>
    /// <param name="partial">True if the sub-run is partial run</param>
    public SubRun (int index, int offset, int length, bool partial)
    {
        Index = index;
        Offset = offset;
        Length = length;
        Partial = partial;
    }

    /// <summary>
    /// The index of the run in the list of runs
    /// </summary>
    public int Index;

    /// <summary>
    /// Offset of this sub-run in the containing run
    /// </summary>
    public int Offset;

    /// <summary>
    /// Length of this sub-run in the containing run
    /// </summary>
    public int Length;

    /// <summary>
    /// Indicates if this sub-run is partial sub-run
    /// </summary>
    public bool Partial;
}

/// <summary>
/// Helpers for iterating over a set of consecutive runs
/// </summary>
public static class RunExtensions
{
    /// <summary>
    /// Given a list of consecutive runs, a start index and a length
    /// provides a list of sub-runs in the list of runs.
    /// </summary>
    /// <typeparam name="T">The list element type</typeparam>
    /// <param name="list">The list of runs</param>
    /// <param name="offset">The offset of the run</param>
    /// <param name="length">The length of the run</param>
    /// <returns>An enumerable collection of SubRuns</returns>
    public static IEnumerable<SubRun> GetInterectingRuns<T> (this IReadOnlyList<T> list, int offset, int length)
        where T : IRun
    {
        // Check list is consistent
        list.CheckValid();

        // Calculate end position
        var to = offset + length;

        // Find the start run
        var startRunIndex = list.BinarySearch (offset, (r, a) =>
        {
            if (r.Offset > a)
            {
                return 1;
            }

            if (r.Offset + r.Length <= a)
            {
                return -1;
            }

            return 0;
        });
        Debug.Assert (startRunIndex >= 0);
        Debug.Assert (startRunIndex < list.Count);

        // Iterate over all runs
        for (var i = startRunIndex; i < list.Count; i++)
        {
            // Get the run
            var r = list[i];

            // Quit if past requested run
            if (r.Offset >= to)
            {
                break;
            }

            // Yield sub-run
            var sr = new SubRun();
            sr.Index = i;
            sr.Offset = i == startRunIndex ? offset - r.Offset : 0;
            sr.Length = Math.Min (r.Offset + r.Length, to) - r.Offset - sr.Offset;
            sr.Partial = r.Length != sr.Length;
            yield return sr;
        }
    }

    /// <summary>
    /// Given a list of consecutive runs, a start index and a length
    /// provides a list of sub-runs in the list of runs (in reverse order)
    /// </summary>
    /// <typeparam name="T">The list element type</typeparam>
    /// <param name="list">The list of runs</param>
    /// <param name="offset">The offset of the run</param>
    /// <param name="length">The length of the run</param>
    /// <returns>An enumerable collection of SubRuns</returns>
    public static IEnumerable<SubRun> GetIntersectingRunsReverse<T> (this IReadOnlyList<T> list, int offset, int length)
        where T : IRun
    {
        // Check list is consistent
        list.CheckValid();

        // Calculate end position
        var to = offset + length;

        // Find the start run
        var endRunIndex = list.BinarySearch (to, (r, a) =>
        {
            if (r.Offset >= a)
            {
                return 1;
            }

            if (r.Offset + r.Length < a)
            {
                return -1;
            }

            return 0;
        });
        Debug.Assert (endRunIndex >= 0);
        Debug.Assert (endRunIndex < list.Count);

        // Iterate over all runs
        for (var i = endRunIndex; i >= 0; i--)
        {
            // Get the run
            var r = list[i];

            // Quit if past requested run
            if (r.Offset + r.Length <= offset)
            {
                break;
            }

            // Yield sub-run
            var sr = new SubRun();
            sr.Index = i;
            sr.Offset = r.Offset > offset ? 0 : offset - r.Offset;
            sr.Length = Math.Min (r.Offset + r.Length, to) - r.Offset - sr.Offset;
            sr.Partial = r.Length != sr.Length;
            yield return sr;
        }
    }

    /// <summary>
    /// Get the total length of a list of consecutive runs
    /// </summary>
    /// <typeparam name="T">The element type</typeparam>
    /// <param name="list">The list of runs</param>
    /// <returns>The total length</returns>
    public static int TotalLength<T> (this IReadOnlyList<T> list) where T : IRun
    {
        // Empty list?
        if (list.Count == 0)
        {
            return 0;
        }

        // Get length from last element
        var last = list[list.Count - 1];
        return last.Offset + last.Length;
    }

    /// <summary>
    /// Check that a list of runs is valid
    /// </summary>
    /// <typeparam name="T">The element type</typeparam>
    /// <param name="list">The list to be checked</param>
    [Conditional ("DEBUG")]
    public static void CheckValid<T> (this IReadOnlyList<T> list) where T : IRun
    {
        CheckValid (list, list.TotalLength());
    }

    /// <summary>
    /// Check that a list of runs is valid
    /// </summary>
    /// <typeparam name="T">The element type</typeparam>
    /// <param name="list">The list to be checked</param>
    /// <param name="totalLength">The expected total length of the list of runs</param>
    [Conditional ("DEBUG")]
    public static void CheckValid<T> (this IReadOnlyList<T> list, int totalLength) where T : IRun
    {
        if (list.Count > 0)
        {
            // Must start at zero
            Debug.Assert (list[0].Offset == 0);

            // Must cover entire code point buffer
            Debug.Assert (list[list.Count - 1].Offset + list[list.Count - 1].Length == totalLength);

            var prev = list[0];
            for (var i = 1; i < list.Count; i++)
            {
                // All runs must have a length
                Debug.Assert (list[i].Length > 0);

                // All runs must be consecutive and joined end to end
                Debug.Assert (list[i].Offset == prev.Offset + prev.Length);

                prev = list[i];
            }
        }
        else
        {
            // If no style runs then mustn't have any code points either
            Debug.Assert (totalLength == 0);
        }
    }
}
